'''
==================================
Conversion to/from JSON structures
==================================

*Pyson* has support for encoding or decoding arbitrary Python objects to JSON.
JSON to object encoders and decoders are simple functions that convert python
to JSON structures or vice-versa. 

Usage
-----

As an example, we implement *ad-hoc* support for converting ``datetime.date``
objects to/from JSON. We start by implementing the encoder and decoder 
functions. 

The encoder function must return a valid JSON type. In most cases, it will be
a list or dictionary representing the input object. The user should not
encode the sub-items of the resulting list or dictionary. This is done 
automatically in the global encoder function.

It is acceptable to return arbitrary mapping or sequences objects instead of 
regular dictionaries and lists. These objects must be mutable. It is not safe 
to return "views" to the input object data as the encoder function expects that 
changes to the output should not produce side-effects in the input object.

One use-case for using non-standard types is to return an OrderedDict instance
instead of a regular dictionary. This way, the fields in the JSON structure are
saved in a predictable order.  

We shall represent the `datetime.date` object simply as a list [YYYY, MM, DD]. 
Hence the encoder simply convert the `datetime.date` object into a list:

>>> def encode_date(obj):
...     return list(obj.timetuple()[:3])

The decoder function takes the JSON structure that was returned by the encoder,
and constructs the desired Python object. When the JSON structure hits the 
user-defined encoder function, the sub items of dictionaries and lists will be
already encoded. The user simply has to revert the operation done by the encoder
function that was previously defined.  

Back to the example with a `datetime.date` object, the implementation of the 
decoder function is very straightforward. 

>>> def decode_date(json):
...     return date(*json)

These functions must be registered either in a local context or in the global
context. Each type that is registered into the system must be given a unique 
name. This name is added into the encoded JSON structures to hint the system
how these structures should be converted back into a Python object. 

A valid name is a sequence of Python identifiers joint by dots. It is a good 
practice to choose the name as the shortest import that can load the desired 
type. This is not computed automatically because API's can change and modules
and packages may be reorganized from time to time. If a new version of a package
defines some type in a different module, then things would be likely to break
if names were given automatically. 

We shall choose 'datetime.date' as the qualified name:

>>> from datetime import date
>>> register(date, 'datetime.date', encoder=encode_date, decoder=decode_date)

Now we can easily convert 'datetime.date' objects to JSON:

>>> json_encode(date(2012, 12, 12))
[2012, 12, 12, '@type:datetime.date']

The last item in the returned list is a type hint for the system. We can encode
objects returned by json_encode() automatically:

>>> json_decode(json_encode(date(2012, 12, 12)))
datetime.date(2012, 12, 12)

The type hint must be given manually if the input object was created manually 
by the user, and does not have a type hint. 

>>> json_decode([2012, 12, 12])
[2012, 12, 12]

>>> json_decode([2012, 12, 12], 'datetime.date')
datetime.date(2012, 12, 12)
'''

import re
import decimal
import functools
import types
from pyson.types import Object, Array
from pyson.type_name_utils import type2name

#__all__ = ['register', 'json_decode', 'callfunc']

class jsoncontext(object):
    def __init__(self, ammend=False):
        self._encoders = {}
        self._decoders = {}
        self._locked = False
        if ammend:
            self.update(GLOBAL_CONTEXT)

    _register_builtin = False

    #===========================================================================
    # Registering functions
    #===========================================================================
    def register(self, tt, name, encoder, decoder, overwrite=False):
        '''Register a converter function for the given type.
        
        Parameters
        ----------
        tt : type
            Class of type to be registered.
        function : callable
            Function that performs the conversion. See func:`unjsonfy` for the 
            allowed function signatures.
        overwrite : bool
            Forces overwrite of a previous converter for the type ``tt`` if it 
            exists. 
            
        Register a converter function for the given type.
        
        Parameters
        ----------
        tt : type
            Class of type to be registered.
        function : callable
            Function that performs the conversion. See func:`unjsonfy` for the 
            allowed function signatures.
        overwrite : bool
            Forces overwrite of a previous converter for the type ``tt`` if it 
            exists. 
        '''
        self._assure_unlocked()

        if name is None:
            name = '@' + self._type2name(tt)
        else:
            self._validate_name(name)

        if tt in self._decoders or name in self._decoders:
            if not overwrite:
                raise ValueError("type %s is already registered" % name)
            else:
                raise NotImplementedError

        self._add(tt, name, encoder, decoder)

    def register_cls(self, cls=None, name=None, encoder_method=None,
                     decoder_method=None, reg_global=False):
        '''Works as a decorator that register a class from the given encode 
        and decode class (or static) methods. 
        
        **Encoder and Decoder methods**
        
        The parameters 'encoder_method' and 'decoder_method' should tell the 
        methods names. If not given, it searches first for 'json_encode' 
        and 'json_decode'. Secondly, it tries the variations '_json_(en,de)code', 
        '_json_(en,de)code_', and finally '__json_(en,de)code__'.
        
        The decoder method must be a class method or static method. This 
        restriction is necessary because we need to create an instance of the
        class from a JSON structure, and there may be no instance available
        to call the method from.
        
        The encoder method can be an ordinary method which is called with no 
        arguments (in which case it encodes ``self``), or a class method or 
        a static method that takes a class instance as its single input.  
        
        Examples
        --------
        
        >>> @register_cls('my_module.Point')
        ... class Point(object):
        ...   def __init__(self, x, y):
        ...       self.x, self.y = x, y
        ...   
        ...   def json_encode(self):
        ...       return [self.x, self.y]
        ... 
        ...   @classmethod
        ...   def json_decode(cls, obj):
        ...       return Point(*obj)
        >>> json_encode(Point(1, 2))
        [1, 2, '@type:my_module.Point']
        '''
        self._assure_unlocked()

        if isinstance(cls, basestring) and name is None:
            cls, name = name, cls

        if cls is None:
            if reg_global is None and self is GLOBAL_CONTEXT:
                reg_global = True

            def register_decorator(cls):
                self.register_cls(cls, name, encoder_method, decoder_method, reg_global)
                return cls
            return register_decorator

        # Encoder and decoder methods
        methods = set(dir(cls))
        if encoder_method is None:
            for m in ['json_encode', '_json_encode', '_json_encode_', '__json_encode__']:
                if m in methods:
                    encoder_method = m
                    break
            else:
                raise ValueError('could not find encoder method')


        if decoder_method is None:
            for m in ['json_decode', '_json_decode', '_json_decode_', '__json_decode__']:
                if m in methods:
                    decoder_method = m
                    break
            else:
                raise ValueError('could not find decoder method')

        # Check encoder method types
        encoder = getattr(cls, encoder_method)
        decoder = getattr(cls, decoder_method)
        if isinstance(decoder, types.MethodType) and decoder.im_self is None:
            raise TypeError('decoder method must be a class method or an static method')

        # Register in base or only in local context?
        try:
            self._base._locked = False
            if reg_global:
                self = self._base
            try:
                self.register(cls, name, encoder, decoder)
            except ValueError:
                if cls.__module__ == '__main__':
                    import warnings
                    warnings.warn('cannot register classes defined in the main module: skipping %s' % cls.__name__)
        finally:
            self._base._locked = True

    def auto_register(self, encoding):
        '''Searches for an appropriate decoder/encoder and register it.
        It returns False if no encoder/decoder is found.'''

        # Checks if type is registered in base
        base = self._base
        if encoding in base._encoders:
            encoder, tt, name = base._encoders[encoding]
            decoder, tt, name = base._decoders[encoding]
            self._add(tt, name, encoder, decoder)
            return True

        # See if there is a converter in a pyson.json_encoding.ext_* module
        # These modules register types on base
        name = type2name(encoding) if isinstance(encoding, type) else encoding
        name = name[1:] if name.startswith('@') else name

        try:
            mod = 'pyson.json_encoding.ext_' + name.split('.')[0]
            __import__(mod)
        except ImportError:
            pass
        else:
            encoder, tt, name = base._encoders[encoding]
            decoder, tt, name = base._decoders[encoding]
            self._add(tt, name, encoder, decoder)
            return True

#        # Inspect the class to see if it has a converter method
#        try:
#            name = getattr(tt, '_json_name_', '@' + type2name(tt))
#            converter = DECODERS[tt] = (tt._json_decode_, name)
#            return converter
#        except AttributeError:
#            pass
#        if hasattr(tt, '_from_json_'):
#            raise DeprecationWarning('on type %s: _from_json_ was changed to _json_decode_' % tt)
#
#
#        # Check the superclass
#    #    try:
#    #        converter = CONVERTERS.cache[(tt,)] = CONVERTERS.super(tt)
#    #        return converter
#    #    except DispatchError:
#        raise TypeError("can't get json decoder for %s object" % tt)
#
#        if tt is not None:
#            if isinstance(tt, type):
#                decoder = get_decoder_from_type(tt)
#                return decoder(json)
#            else:
#                raise TypeError('tt must be a type, got %s' % type(tt))
#
#        return json_decode_worker(json)

    def _add(self, tt, name, encoder, decoder):
        self._decoders[name] = (decoder, tt, name)
        self._decoders[tt] = (decoder, tt, name)
        self._encoders[name] = (encoder, tt, name)
        self._encoders[tt] = (encoder, tt, name)

    _NAME_RE = re.compile('^[a-zA-Z]([a-zA-Z0-9_]*)([.][a-zA-Z][a-zA-Z0-9_])+')
    _BUILTIN_NAME_RE = re.compile('^[a-zA-Z]([a-zA-Z0-9_]*)')
    def _validate_name(self, name):
        regex = self._NAME_RE if not self._register_builtin else self._BUILTIN_NAME_RE
        if not regex.match(name):
            raise ValueError('invalid name: %s' % name)

    #===========================================================================
    # Encoding/decoding
    #===========================================================================
    def json_decode(self, json, decoder=None):
        '''Create object from its JSON representation
        
        Parameters
        ----------
        json : JSON-like
            JSON-like structure to be converted to a Python object.
        tt (optional): type
            Type of the output object (can be inferred from the '@type' key, if 
            ``json`` has it. 
        '''

        decode = self.json_decode

        # Dictionary structure
        if isinstance(json, Object):
            json = { k: decode(v) for (k, v) in json.items() }
            try:
                decoder = (json.pop('@type') if decoder is None else decoder)
            except KeyError:
                pass

        # Array structure
        elif isinstance(json, Array):
            json = [ decode(x) for x in json ]
            if json and decoder is None:
                last = json[-1]
                if isinstance(last, basestring) and last.startswith('@type:'):
                    decoder = json.pop()[6:]

        # Fetch decoder, if necessary
        if decoder is None:
            return json
        else:
            try:
                decoder, tt, _name = self._decoders[decoder]
            except KeyError:
                if self.auto_register(decoder):
                    decoder, tt, _name = self._decoders[decoder]
                else:
                    raise ValueError('type/name does not support decoding: %s' % decoder)

        # Compute result and check if it is of the right type
        result = decoder(json)
        assert type(result) is tt, 'inconsistent output type: expected %s, but got %s' % (tt, type(result))
        return result

    def json_encode(self, json, encoder=None, type_hint=True):
        '''Creates a JSON structure that represents a Python object.
        
        Parameters
        ----------
        json : JSON-like
            JSON-like structure to be converted to a Python object.
        tt (optional): type
            Type of the output object (can be inferred from the '@type' key, if 
            ``json`` has it. 
        '''

        if encoder is None:
            encoder = type(json)
        try:
            encoder, tt, name = self._encoders[encoder]
        except KeyError:
            if self.auto_register(encoder):
                encoder, tt, name = self._encoders[encoder]
            else:
                raise ValueError('encoder not supported: %s' % encoder)

        # Encode the output
        result = encoder(json)
        encode = self.json_encode
        if isinstance(result, Object):
            result.update({ k: encode(v) for (k, v) in result.items() })
        elif isinstance(result, Array):
            result[:] = [ encode(v) for v in result ]

        # Compute result and add the type hint string
        if type_hint:
            if isinstance(result, Object) and tt is not dict:
                result[u'@type'] = name
            elif isinstance(result, Array) and tt is not list:
                result.append('@type:%s' % name)
            return result
        else:
            return result

    #===========================================================================
    # Context manager support
    #===========================================================================
    def __enter__(self):
        global GLOBAL_CONTEXT
        self.__old_global_ctx = GLOBAL_CONTEXT
        GLOBAL_CONTEXT = self

    def __exit__(self, ex_tt, ex_val, ex_tb):
        global GLOBAL_CONTEXT
        GLOBAL_CONTEXT = self.__old_global_ctx

        return False

    #===========================================================================
    # Locking mechanism
    #===========================================================================
    def lock(self):
        '''Locks context in order to avoid registering new encoders/decoders.
        
        Locked contexts cannot be unlocked.'''
        self._locked = True

    @property
    def locked(self):
        return self._locked

    def _assure_unlocked(self):
        if self.locked:
            raise ValueError("trying to modify a locked context")

#===============================================================================
# Auxiliary factory functions
#===============================================================================
def attr_fields(attrs, allow_missing=False):
    '''Factory function for converters that take attributes from an object and
    return a dictionary with (attr_name, attr_value) pairs.'''

    attrs = tuple(attrs)

    if allow_missing:
        def converter(obj):
            result = {}
            for attr in attrs:
                try:
                    result[attr] = getattr(obj, attr)
                except AttributeError:
                    pass
            return result
    else:
        def converter(obj):
            values = map(functools.partial(getattr, obj), attrs)
            return dict(zip(attrs, values))

    return converter

def callfunc(tt, json_t=dict):
    '''Factory function for converter methods that call a function using 
    object as *args or **kwds depending on the value of json_t variable.'''

    if json_t is dict:
        def converter(obj):
            kwds = obj.copy()
            kwds.pop('@type', None)
            return tt(**kwds)
    elif json_t is list:
        def converter(obj):
            args = obj
            return tt(*args)
    else:
        raise ValueError('json_t must be list or dict')

    return converter

#===============================================================================
# Base context: a fallback context holding the most common python types 
#===============================================================================
_BASE_CONTEXT = object.__new__(jsoncontext)
_BASE_CONTEXT._decoders = {}
_BASE_CONTEXT._encoders = {}
_BASE_CONTEXT._locked = False
jsoncontext._base = _BASE_CONTEXT
_BASE_CONTEXT.lock()

def _register_builtin(tt, name, encoder, decoder):
    if tt not in _BASE_CONTEXT._encoders:
        try:
            _BASE_CONTEXT._register_builtin, _BASE_CONTEXT._locked = True, False
            _BASE_CONTEXT.register(tt, name, encoder, decoder)
        finally:
            _BASE_CONTEXT._register_builtin, _BASE_CONTEXT._locked = False, True
    assert tt in _BASE_CONTEXT._encoders

# Atomic values
for tt in [unicode, int, bool, type(None)]:
    _register_builtin(tt, tt.__name__, encoder=lambda x: x, decoder=lambda x: x)
_register_builtin(str, 'str', encoder=lambda x: x.decode('utf8'), decoder=lambda x: x)

# Floats and decimals
_register_builtin(float, 'float', encoder=decimal.Decimal, decoder=lambda x: x)
_register_builtin(decimal.Decimal, 'real', encoder=lambda x: x, decoder=lambda x: x)

# Lists and dicts
_register_builtin(list, 'list', encoder=list, decoder=lambda x: x)
_register_builtin(dict, 'dict', encoder=dict, decoder=lambda x: x)

# Common python types: sequences
for tt in [tuple, set, frozenset]:
    _register_builtin(tt, tt.__name__, encoder=list, decoder=tt)

del tt

#===============================================================================
# Global context methods
#===============================================================================
class GlobalContext(jsoncontext):
    def __init__(self):
        super(GlobalContext, self).__init__(False)

    def lock(self):
        raise TypeError('global context cannot be locked')

GLOBAL_CONTEXT = GlobalContext()

def global_context():
    '''Return the global context'''
    return GLOBAL_CONTEXT

def global_ctx_method(func):
    '''Decorates function to match the corresponding method from global 
    context'''

    name = func.func_name
    func.func_doc = getattr(GLOBAL_CONTEXT, name)
    return func

@global_ctx_method
def register(tt, name, encoder, decoder, overwrite=False):
    return GLOBAL_CONTEXT.register(tt, name, encoder, decoder, overwrite)

@global_ctx_method
def register_cls(cls=None, name=None, encoder_method=None, decoder_method=None, reg_global=None):
    return GLOBAL_CONTEXT.register_cls(cls, name, encoder_method, decoder_method, reg_global)

@global_ctx_method
def json_decode(json, decoder=None):
    return GLOBAL_CONTEXT.json_decode(json, decoder)

@global_ctx_method
def json_encode(json, encoder=None, type_hint=True):
    return GLOBAL_CONTEXT.json_encode(json, encoder, type_hint)

if __name__ == '__main__':
    assert json_decode({}) == {}
    assert json_decode(['foo']) == ['foo']
    assert json_decode(['foo', 'bar']) == ['foo', 'bar']
    assert json_decode({'foo': 'bar'}) == {'foo': 'bar'}
    assert json_decode([1, 2, u'@type:tuple']) == (1, 2)
    assert json_decode([1, 2], tuple) == (1, 2)

    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)
